package org.vaadin.nf4v.core.navigation;

import com.vaadin.ui.Component;
import com.vaadin.ui.UriFragmentUtility;
import com.vaadin.ui.VerticalLayout;
import org.vaadin.nf4v.core.NavigableApplication;
import org.vaadin.nf4v.core.NavigationException;
import org.vaadin.nf4v.core.config.PageConfig;
import org.vaadin.nf4v.core.handler.*;
import org.vaadin.nf4v.core.navigation.event.EventSupport;
import org.vaadin.nf4v.core.navigation.event.NavigationListener;
import org.vaadin.nf4v.core.navigation.event.NavigationState;
import org.vaadin.nf4v.core.navigation.resource.ContextResource;
import org.vaadin.nf4v.core.navigation.target.NavigationTarget;

/**
 * Navigator is UI area which is used to show pages.
 * Navigator should be placed as usual UI component into NavigableWindow layout.
 * <p/>
 * Navigations may be initiated using generic navigateTo() method (and actually all calls to NavigationManager#navigateTo()
 * are just shortcuts for this method).
 * Component internally uses Vaadin UriFragmentUtility to react for browser address bar uri changes.
 * <p/>
 * Component holds all navigation request lifecycle (validating target, creation of request, context change etc.).
 */
public class Navigator extends VerticalLayout implements UriFragmentUtility.FragmentChangedListener {
    //todo separate ui representation and lifecycle mechanism

    protected UriFragmentUtility uriFragmentUtility;
    protected Component currentPage;
    protected EventSupport support = new EventSupport();
    // used to prevent concurrent requests
    protected NavigationRequest currentRequest;
    // holds target object of current page.
    // target will change only after successful navigation.
    protected NavigationTarget currentTarget;
    // key which is used to store request object in flash during context change
    protected static final String PENDING_REQUEST_KEY = "org.vaadin.nf4v.PENDING_REQUEST";

    public Navigator() {
        setSizeFull();
        uriFragmentUtility = new UriFragmentUtility();
        uriFragmentUtility.addListener(this);
        addComponent(uriFragmentUtility);
        setExpandRatio(uriFragmentUtility, 0);
    }

    /**
     * Registers navigation listener for current NavigableWindow.
     *
     * @param listener listener to register
     * @param states   navigation states to listen (if empty - register to all states)
     */
    public void addNavigationListener(NavigationListener listener, NavigationState... states) {
        support.addNavigationListener(listener, states);
    }

    /**
     * @param listener listener to remove
     * @param states   navigation states to remove from (if empty - remove from all states)
     */
    public void removeNavigationListener(NavigationListener listener, NavigationState... states) {
        support.removeNavigationListener(listener, states);
    }

    /**
     * @return current page instance (currently visible page)
     */
    public Component getCurrentPage() {
        return currentPage;
    }

    /**
     * If no navigation request is currently in process it will return navigation target of current page
     * (target of last navigation request)
     * If navigation request is in process it will return target of navigation.
     *
     * @return current navigation target or null if no page is activated (window is empty and no request in process.
     */
    public NavigationTarget getCurrentTarget() {
        return currentRequest != null && !currentRequest.isCancelled() ? currentRequest.getTarget() : currentTarget;
    }

    /**
     * ALmost the same as getCurrentTarget() but ALWAYS return target of currently active page,
     * until new navigation request will complete.
     *
     * @return current navgation target or nill if no page is activated (window is empty.
     */
    public NavigationTarget getCurrentPageTarget() {
        return currentTarget;
    }

    /**
     * Generic method to execute navigation to another page.
     * Use NavigationManager.navigateTo* shortcut methods (which automatically prepares navigation target)
     *
     * @param target navigation target (target page)
     */
    public void navigateTo(NavigationTarget target) {
        if (currentRequest != null) {
            if (!currentRequest.isCancelled() && currentRequest.getState().ordinal() >= NavigationState.BEFORE_NAVIGATION.ordinal()) {
                throw new NavigationException("Can't navigate to " + target + " cause current navigation to " +
                        currentRequest.getTarget() + " can't be cancelled");
            } else {
                currentRequest.cancel();
            }
        }
        try {
            analyzeTarget(target);
            initiateRequest(target, RequestInitiatorType.API);
        } catch (Throwable ex) {
            NavigableApplication.getCurrent().getNavigationConfig()
                    .getHandler(NavigationUncaughtErrorHandler.class, NavigationManager.getNavigationContext())
                    .onUncaughtError(ex);
        }
    }

    // main point of request target recognition after navigation target builder

    /**
     * Analyzes navigation target for correctness. (currently only for API calls)
     *
     * @param target navigation target
     * @throws NavigationException on wrong target
     */
    protected void analyzeTarget(NavigationTarget target) throws NavigationException {
        // todo move logic from uri change here
        // todo introduce handler to support dynamic pages
        String context = target.getContext() != null ? target.getContext() : NavigationManager.getNavigationContext();
        if (target.getRequireUriChange() == null) {
            target.setRequireUriChange(true);
        }
        // todo search by uri
        // todo cross uri logic
        if (target.isHomePage()) {
            target.setPageClass(NavigableApplication.getCurrent().getNavigationConfig()
                    .getHandler(HomePageResolver.class, context).resolveHomePage(context));
            PageConfig pageConfig = NavigableApplication.getCurrent().getNavigationConfig().getPageConfig(
                    context, target.getPageClass());
            target.setUri(pageConfig.getUri());
        } else if (target.getPageClass() != null) {
            Class<? extends Component> pageClass = target.getPageClass();
            PageConfig pageConfig = NavigableApplication.getCurrent().getNavigationConfig().getPageConfig(
                    context, pageClass);
            if (pageConfig == null) {
                throw new NavigationException("No page with class '" + pageClass.getName() + "' is registered for context '" +
                        context + "'");
            }
            target.setPageClass(pageClass);
            target.setUri(pageConfig.getUri());
        } else {
            throw new NavigationException("Incorrect navigation point " + target);
        }

        if (target.getParams() != null && target.getParamsString() == null) {
            //api navigation may require uri change. preparing everything here
            NavigableApplication.getCurrent().getNavigationConfig()
                    .getHandler(ParamResolver.class, context, target.getPageClass()).prepareParametersString(target);
        }
    }

    // todo reload current page method

    /**
     * Listens uri changes and initiate navigation request for changed uri
     * {@inheritDoc}
     */
    @Override
    public void fragmentChanged(UriFragmentUtility.FragmentChangedEvent source) {
        String context = null;
        try {
            String fragment = source.getUriFragmentUtility().getFragment();

            context = NavigationManager.getNavigationContext();
            if (context == null) {
                throw new IllegalStateException("Can't detect current context");
            }
            // recognize target page
            NavigationTarget target = null;
            try {
                target = NavigableApplication.getCurrent().getNavigationConfig()
                        .getHandler(UriHandler.class, context).parseUri(fragment);
            } catch (InvalidUriException ex) {
                NavigableApplication.getCurrent().getNavigationConfig()
                        .getHandler(InvalidUriHandler.class, context).handleInvalidUri(fragment, ex);
            }

            if (target != null) {
                // parse parameters
                if (target.getParamsString() != null) {
                    try {
                        NavigableApplication.getCurrent().getNavigationConfig()
                                .getHandler(ParamResolver.class, context, target.getPageClass())
                                .resolveParameters(target);
                    } catch (InvalidUriException ex) {
                        NavigableApplication.getCurrent().getNavigationConfig()
                                .getHandler(InvalidUriHandler.class, context).handleInvalidUri(fragment, ex);
                    }
                }
                if (target.getRequireUriChange() == null) {
                    target.setRequireUriChange(false);
                }
                if (getCurrentPage() == null && target.isHomePage()) {
                    NavigationRequest request = new NavigationRequestImpl(target, NavigationState.CONTEXT_ACTIVATED, RequestInitiatorType.URI);
                    if (!fireEvent(request)) {
                        internalRequest(request);
                    }
                } else {
                    initiateRequest(target, RequestInitiatorType.URI);
                }
            }
        } catch (Throwable ex) {
            NavigableApplication.getCurrent().getNavigationConfig()
                    .getHandler(NavigationUncaughtErrorHandler.class, context).onUncaughtError(ex);
        }
    }

    /**
     * Creates navigation request and starts navigation request lifecycle.
     * This is common step for API and URI initiated navigations (except case when new context is just activated through URI).
     * Also detects if context change is required and initiates context changing.
     *
     * @param target navigation target
     * @param type   navigation request initiator type
     */
    protected void initiateRequest(NavigationTarget target, RequestInitiatorType type) {
        NavigationRequest request = new NavigationRequestImpl(target, NavigationState.CONTEXT_NAVIGATION, type);
        if (target.getContext() != null && !NavigationManager.isContext(target.getContext())) {
            startCrossContextNavigation(request, target.isRequireNewTab() ? "_blank" : "_self");
        } else if (target.isRequireNewTab()) {
            startCrossContextNavigation(request, "_blank");
        } else {
            internalRequest(request);
        }
    }

    /**
     * Called if context change is required for reaching navigation target.
     * Fires CONTEXT_NAVIGATION event and changes current window (or opens separate window, depending
     * on target)
     *
     * @param request      navigation request
     * @param targetWindow target window description (either new window (_blank) or current (_self))
     */
    protected void startCrossContextNavigation(NavigationRequest request, String targetWindow) {
        if (!fireEvent(request)) {
            // navigation cancelled
            return;
        }
        // todo weak point: possibly may override some other cross context navigation in different window,
        // cause flash is shared for application
        NavigationManager.flash().put(PENDING_REQUEST_KEY, request);
        getWindow().open(new ContextResource(request.getTarget()), targetWindow);
    }

    /**
     * Called by NavigableApplication after context activation to check if it was part of cross context navigation.
     * Fires CONTEXT_ACTIVATED (if navigation can be continued)
     * <p/>
     * If new context is just created initiates home page activation with casting of CONTEXT_ACTIVATED before it.
     * (home page navigation will be triggered only if navigator didn't contain any page)
     *
     * @param homePageSuspicious indicates suspicious of home page activation requirement
     */
    public void handleContextActivation(boolean homePageSuspicious) {
        // checking stored cross context navigation
        // todo weak point: what if this request will be changed and new navigation with cross context will be initiated?
        NavigationRequestImpl request = (NavigationRequestImpl) NavigationManager.flash().remove(PENDING_REQUEST_KEY);
        if (request != null
                && request.getTarget().getContext() == null
                && !request.getTarget().getContext().equals(NavigationManager.getNavigationContext())) {

            try {
                request.setState(NavigationState.CONTEXT_ACTIVATED);
                if (!fireEvent(request)) {
                    // case when cross context navigation found but event listener cancels page navigation after context change
                    // now it is listener responsibility to redirect request correctly (in other way no page could be displayed)
                    return;
                }
                internalRequest(request);
            } catch (Throwable ex) {
                NavigableApplication.getCurrent().getNavigationConfig()
                        .getHandler(NavigationUncaughtErrorHandler.class, NavigationManager.getNavigationContext())
                        .onUncaughtError(ex);
            }
            return;
        }

        if (homePageSuspicious && getCurrentPage() == null) { // context window not set yet (empty context)
            initiateNewContextActivation();
        }
    }

    /**
     * Creates navigation request for current context home page and casts CONTEXT_ACTIVATED.
     */
    protected void initiateNewContextActivation() {
        NavigationTarget target = NavigationManager.navigateTo().home().build();
        try {
            analyzeTarget(target);
            NavigationRequest request = new NavigationRequestImpl(target,
                    NavigationState.CONTEXT_ACTIVATED,
                    RequestInitiatorType.URI); // in case of api cross context navigation request object will be always stored in flash
            if (!fireEvent(request)) {
                // case when cross context navigation found but event listener cancels page navigation after context change
                // now it is listener responsibility to redirect request correctly (in other way no page could be displayed)
                return;
            }
            internalRequest(request);
        } catch (Throwable ex) {
            NavigableApplication.getCurrent().getNavigationConfig()
                    .getHandler(NavigationUncaughtErrorHandler.class, NavigationManager.getNavigationContext())
                    .onUncaughtError(ex);
        }
    }

    /**
     * This is navigation request start point for in-context navigation (for cross-context request after context change)
     * Detects if it is new page navigation or just changing parameters of current page
     * Fires:
     * 1. PAGE_NAVIGATION or PAGE_PARAMETERS_CHANGE
     * 2. BEFORE_NAVIGATION
     * 3. AFTER_NAVIGATION
     *
     * @param request navigation request
     */
    @SuppressWarnings("unchecked")
    protected void internalRequest(NavigationRequest request) {
        NavigationRequestImpl reqImpl = (NavigationRequestImpl) request;
        // current request is set only for navigation in current context (or for page navigation after context change(cross context navigation)
        currentRequest = reqImpl;
        if (currentPage == null || !currentTarget.getPageClass().equals(reqImpl.getTarget().getPageClass())) { // We need to change to a new page
            // We don't call navigateTo(), because we don't want the uri to be changed (we are just answering a change notification).
            reqImpl.setState(NavigationState.PAGE_NAVIGATION);
        } else {
            // We don't re-instantiate the page, we just warn it that its parameters changed.
            reqImpl.setState(NavigationState.PAGE_PARAMETERS_CHANGE);
        }

        if (fireEvent(reqImpl)) {
            if (reqImpl.getState() == NavigationState.PAGE_NAVIGATION || reqImpl.getTarget().isRequirePageInstanceCreation()) {
                if (!instantiatePage(reqImpl)) {
                    currentRequest = null;
                    return;
                }
            } else {
                reqImpl.setPageInstance(currentPage);
            }

            //on this state request can't be cancelled
            reqImpl.setState(NavigationState.BEFORE_NAVIGATION);
            fireEvent(reqImpl);
            processRequest(reqImpl);
            reqImpl.setState(NavigationState.AFTER_NAVIGATION);
            fireEvent(reqImpl);
        }
        // this will either assign target to itself or set target of successful request
        currentTarget = getCurrentTarget();
        currentRequest = null;
    }

    /**
     * Page instantiation phase. Uses PageFactory handler to instantiate a new page.
     * Fires PAGE_INSTANTIATED
     *
     * @param request navigation request
     * @return true if request can continue, false if request is cancelled
     */
    protected boolean instantiatePage(NavigationRequest request) {
        NavigationRequestImpl reqImpl = (NavigationRequestImpl) request;
        reqImpl.setPageInstance(NavigableApplication.getCurrent().getNavigationConfig()
                .getHandler(PageFactory.class, request)
                .createPage(reqImpl.getTarget()));
        reqImpl.setState(NavigationState.PAGE_INSTANTIATED);
        return fireEvent(reqImpl);
    }

    /**
     * Changes current page on the screen and updates URI (if required).
     *
     * @param request navigation request
     */
    protected void processRequest(NavigationRequest request) {
        // todo introduce page transition handler (e.g. for animations)
        if (currentPage == null) {
            currentPage = request.getPageInstance();
            addComponent(currentPage);
            setExpandRatio(currentPage, 1);
        } else if (!request.isParametersChangeRequest() || request.getTarget().isRequirePageInstanceCreation()) {
            // page replacement may be not required for parameter change request 
            replaceComponent(currentPage, currentPage = request.getPageInstance());
            setExpandRatio(currentPage, 1);
        }
        if (request.getTarget().getRequireUriChange()) {
            if (request.getTarget().getUri() != null || request.getTarget().getParams() != null) {
                updateUriFragment(request.getTarget());
            }
        }
    }

    /**
     * Updates URI.
     * Usually called after API navigation to set current page URI (in browser address bar)
     *
     * @param target navigation target
     */
    protected void updateUriFragment(NavigationTarget target) {
        uriFragmentUtility.setFragment(
                NavigableApplication.getCurrent().getNavigationConfig()
                        .getHandler(UriHandler.class, NavigationManager.getNavigationContext())
                        .composeUri(target),
                false);
    }

    /**
     * Fires current request state as event.
     * First process listeners in NavigationManager (global listeners) and then
     * current navigator (current window) listeners
     *
     * @param request navigation request
     * @return true if request can continue, false if request is cancelled
     */
    protected boolean fireEvent(NavigationRequest request) {
        NavigableApplication.getCurrent().getNavigationManager().fireEvent(request);
        if (!request.isCancelled()) {
            support.fireEvent(request);
        }
        return !request.isCancelled();
    }

}
